package microcredit.model;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.OrderBy;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;

import microcredit.gui.util.FormatUtilities;
import microcredit.model.base.Model;
import microcredit.model.domain.LoanExtensionReason;
import microcredit.model.domain.LoanStatus;
import microcredit.model.domain.Status;
import microcredit.model.util.ModelUtils;

/**
 * Entity implementation class for Entity: MemberToLoan
 *
 */
@Entity
public class MemberToLoan  extends Model implements Serializable {

	private static final long serialVersionUID = 1L;

	public static final String SECURITY1 = "security1";
	public static final String SECURITY2 = "security2";
	public static final String SECURITY3 = "security3";
	
	public static final String LOAN_AMOUNT = "loanAmount";
	public static final String NUMBER_OF_WEEKS = "numberOfWeeks";
	public static final String START_DATE = "startDate";
	public static final String SEQUENCE_ID = "sequenceID";
	public static final String INTEREST_PERCENTAGE = "interestPercentage";
	
	public static final String EXTENSION_REASON = "extensionReason";
	public static final String EXTENSION_DESCRIPTION = "extensionDescription";
	
	public static final String REPAYMENT_RATE = "repaymentRate";
	public static final String REPAYMENT_DATE = "repaymentDate";
	
	public static final String PAYMENT = "payment";
	
	public static final int MAXIMUM_NUMBER_OF_WEEKS = 250;
	
	public MemberToLoan() {
		super();
		setLoanStatus(LoanStatus.A);
	}
	
	@Id
	@Column(name="MEMBER_ID", updatable=false, insertable=false)
	private Long memberID;
	
	@Id
	@Column(name="SEQUENCE_ID")
	private Long sequenceID;
	
	@OneToOne()
//	@JoinColumn(name="MEMBER_ID", insertable=false, updatable=false)
    private Member member;
    
	@Column(columnDefinition=MONEY_DECIMAL)
    private BigDecimal loanAmount;
    
	@Column(columnDefinition=MONEY_DECIMAL)
    private BigDecimal amountPaid;
    
	@Column(columnDefinition=MONEY_DECIMAL)
    private BigDecimal interestPercentage;
	
	@Column(columnDefinition=MONEY_DECIMAL)
	private BigDecimal repaymentRate;
    
    @Column(name="SECURITY1_ID", updatable=false, insertable=false)
    private Long security1ID;
    
    @Column(name="SECURITY2_ID", updatable=false, insertable=false)
    private Long security2ID;
    
    @Column(name="SECURITY3_ID", updatable=false, insertable=false)
    private Long security3ID;
    
    @OneToOne
    private Member security1;
    @OneToOne
    private Member security2;
    @OneToOne
    private Member security3;
    
    @Temporal(TemporalType.DATE)
    private Date startDate;
    
    @Temporal(TemporalType.DATE)
    private Date repaymentStartDate;
    
    @Temporal(TemporalType.DATE)
    private Date finalizedDate;
    
    private int numberOfWeeks;
    
    private String description;
	
    @Enumerated(EnumType.STRING)
    @Column(length=1)
    private LoanStatus loanStatus;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Column(nullable=false)
    private Date insertStamp;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Column(nullable=false)
    private Date updateStamp;

    @OneToMany(mappedBy="memberToLoan", fetch=FetchType.EAGER, targetEntity=MemberToLoanHistory.class)
    @OrderBy(value="historySequenceID")
    private List<MemberToLoanHistory> memberToLoanHistories;
    
    @OneToMany(mappedBy="memberToLoan", fetch=FetchType.EAGER, targetEntity=MemberToLoanEntry.class)
    @OrderBy(value="entrySequenceID")
    private List<MemberToLoanEntry> memberToLoanEntries;
    
    @Transient
    private Long customSequenceID;
    
    @Transient
    private Integer historyIndex;
    
    public String createStatusString() {
    	StringBuilder b = new StringBuilder();
		if (isNew()) {
    		b.append("New loan");
    	} else {
    		b.append(getLoanStatus().getString());
    	}
    	return b.toString();
    }
    
    public Integer getHistoryIndex() {
		return historyIndex;
	}
    public void setHistoryIndex(Integer historyIndex) {
		this.historyIndex = historyIndex;
	}
    
    public void setCustomSequenceID(Long customSequenceID) {
		this.customSequenceID = customSequenceID;
	}
    
    public Long getCustomSequenceID() {
		return customSequenceID;
	}
    
    public boolean isEntryFinalized() {
    	List<MemberToLoanEntry> entries = getMemberToLoanEntries();
		if (entries != null) {
			for (int i = 0; i < getSafeNumberOfWeeks(); i++) {
				MemberToLoanEntry entry = entries.get(i);
				if (entry.getLocked() != null && entry.getLocked().booleanValue()) {
					// ignore locked for this check
					continue;
				}
				if (entry.getFixed() != null && entry.getFixed().booleanValue()) {
					return true;
				}
			}
		}
    	return false;
    }
    
    public boolean isPaid() {
    	BigDecimal returnAmount = getReturnAmount();
    	if (getAmountPaid() == null || returnAmount == null) {
    		return false;
    	}
    	return getAmountPaid().compareTo(getReturnAmount()) == 0;
    }
    
    public boolean isFinalized() {
    	return getLoanStatus() == null 
    		|| (getLoanStatus() != LoanStatus.A && getLoanStatus() != LoanStatus.R);
    }
    
    public boolean areAllEntriesFinalized() {
    	if (getSequenceID() == null) {
    		return false;
    	}
    	List<MemberToLoanEntry> entries = getMemberToLoanEntries();
		if (entries != null) {
			for (int i = 0; i < getSafeNumberOfWeeks(); i++) {
				MemberToLoanEntry entry = entries.get(i);
				if (entry.getFixed() == null || !entry.getFixed().booleanValue()) {
					return false;
				}
			}
		}
    	return true;
    }
    
    public boolean hasExtension() {
    	if (getLastHistory() != null) {
    		return getLastHistory().getExtensionReason() != LoanExtensionReason.D;
    	}
    	return false;
    }
    
    public boolean hasNewExtension() {
    	if (getLastHistory() != null) {
    		return getLastHistory().getHistorySequenceID() == null;
    	}
    	return false;
    }
    
	
	public boolean isNew() {
		if (getLoanStatus() == null || getSequenceID() == null) {
			return true;
		}
		return false;
	}
	
	
	public MemberToLoanHistory getLastLastHistory() {
		List<MemberToLoanHistory> histories = getValidMemberToLoanHistories();
		if (histories != null && histories.size() > 1) {
			return histories.get(histories.size()-2);
		}
		return null;
	}
	
	public MemberToLoanHistory getInitialHistory(){
		List<MemberToLoanHistory> histories = getValidMemberToLoanHistories();
		if (histories != null && histories.size() > 0) {
			return histories.get(0);
		}
		return null;
	}
	
	public MemberToLoanHistory getLastHistory() {
		List<MemberToLoanHistory> histories = getValidMemberToLoanHistories();
		if (histories != null && histories.size() > 0) {
			return histories.get(histories.size()-1);
		}
		return null;
	}
    
	@PrePersist
	public void prePersist() throws ValidationException {
		if (getInsertStamp() == null) {
			setInsertStamp(ModelUtils.createTimestamp());
		}
		if (getSequenceID() == null) {
			setSequenceID(getMember().getMemberToLoans().indexOf(this) + 0L);
		}
		setUpdateStamp(getInsertStamp());
		validate();
	}
	
	@PreUpdate
	public void preUpdate() throws ValidationException {
		setUpdateStamp(ModelUtils.createTimestamp());
		validate();
	}
	
	public void validate() throws ValidationException {
		MemberToLoanHistory history = getLastHistory();
		if (history == null) {
			throw new ValidationException("No history available");
		}
		if (getLoanStatus() == null) {
			throw new ValidationException("No loan status provided!", (String)null);
		} else if (getLoanAmount() == null || history.getLoanAmount() == null) {
			throw new ValidationException("No amount provided!", LOAN_AMOUNT);
		} else if (getStartDate() == null || history.getStartDate() == null) {
			throw new ValidationException("No start date provided!", START_DATE);
		} else if (getSecurity1() == null && getSecurity2() == null && getSecurity3() == null) {
			throw new ValidationException("At least one security member should be provided for a loan!", SECURITY1);
		} else if (getNumberOfWeeks() <= 0 || history.getNewNumberOfWeeks() == null) {
			throw new ValidationException("Number of weeks must be at least 1!", NUMBER_OF_WEEKS);
		} else if (getLoanAmount().compareTo(new BigDecimal(getNumberOfWeeks())) < 0) {
			throw new ValidationException("The loan amount must be higher or equal the number of weeks.", NUMBER_OF_WEEKS);
		} else if (ModelUtils.compare(getReturnAmount(), getAmountPaid()) < 0) {
			throw new ValidationException("Too much paid for the loan!", PAYMENT);
		} else if (ModelUtils.compare(getAmountPaid(), new BigDecimal(0)) < 0) {
			throw new ValidationException("The amount paid cannot be negative!", PAYMENT);
		} else if (getMember() != null && getMember().getStatus() != Status.A) {
			throw new ValidationException("The member must be active to create a new loan!", (String)null);
		} else if (getRepaymentStartDate() != null &&
				getRepaymentStartDate().compareTo(getStartDate()) < 0) {
			throw new ValidationException("The repayment date must be after the start date!", MemberToLoan.REPAYMENT_DATE);
		}
		
		BigDecimal returnAmount = getReturnAmount();
		if (getRepaymentRate() != null && getReturnAmount() != null) {
			BigDecimal minRepaymentRate = returnAmount.divide(new BigDecimal(MAXIMUM_NUMBER_OF_WEEKS),2,BigDecimal.ROUND_HALF_UP);
			if (getRepaymentRate().compareTo(minRepaymentRate) < 0) {
				throw new ValidationException("The repayment rate has to be at least '"+FormatUtilities.formatMoney(minRepaymentRate)+"'. <br/>The maximum number of weeks (250) would exceed.", MemberToLoan.REPAYMENT_RATE);
			}
		}
		if (getNumberOfWeeks() > MAXIMUM_NUMBER_OF_WEEKS) {
			throw new ValidationException("The maximum amount of weeks is 250!", MemberToLoan.NUMBER_OF_WEEKS);
		}
		
		
		// ensure that loan entries are sequentially filled up
		validateLoanEntryAmounts();
		
		if (hasExtension()) {
			
			// if new extension validate if properties are set.
			if (history.getExtensionReason() == null) {
				throw new ValidationException("No extension reason defined.", EXTENSION_REASON);
			}
			
			if (history.getExtensionReason() == LoanExtensionReason.O
					&& (history.getReasonText() == null || history.getReasonText().trim().isEmpty())) {
				throw new ValidationException("For extension reason 'Other' a description must be entered!", EXTENSION_DESCRIPTION);
			}
			
			MemberToLoanHistory lastHistory = getLastLastHistory();
			if (lastHistory != null) { // means that the new extension is a loan extension
				
				// validate that loan extension has changed something
				Date lockedDate = calculateLockedStartDate();
//				if (ModelUtils.equals(lastHistory.getNewNumberOfWeeks(), history.getNewNumberOfWeeks())
//						&& ModelUtils.equals(lockedDate, history.getStartDate())) {
//					throw new ValidationException("For a loan extension the number of weeks or the start date must be changed.");
//				}
				int lockedIndex = findEntryLockedIndex();
				if (getLoanStatus() != LoanStatus.N && lockedIndex == getNumberOfWeeks()) {
					throw new ValidationException("You have to provide more than "+(lockedIndex)+" weeks. No loan rates available to fulfill the loan.", NUMBER_OF_WEEKS);
				}
				
				if (ModelUtils.compare(history.getStartDate(), lockedDate) < 0) {
					throw new ValidationException("The extension start date must be after or equal '"+FormatUtilities.formatDate(lockedDate)+"'", START_DATE);
				}
				
				if (ModelUtils.compare(history.getNewNumberOfWeeks(), lockedIndex+1) < 0) {
					throw new ValidationException(
							"Extended number of weeks ("+history.getNewNumberOfWeeks()+") cannot be smaller than the previous number of weeks " +
									"("+lastHistory.getNewNumberOfWeeks()+")", NUMBER_OF_WEEKS);
				}
			}
			
		}
		
	}
	
	public int findEntryLockedIndex() {
		int i = 0;
		if (getMemberToLoanEntries() != null) {
			for (; i < getSafeNumberOfWeeks(); i++) {
				List<MemberToLoanEntry> entries = getMemberToLoanEntries();
				if (i < entries.size()) {
					MemberToLoanEntry entry = entries.get(i);
					if (entry.getLocked() != null && entry.getLocked().booleanValue()) {
						continue;
					} else {
						break;
					}
				} else {
					// member to loan still null we assume that is not locked.
					break;
				}
			}
		}
		return i;
	}
	
	public Date calculateLockedStartDate() {
		Date lastDate = null;
		if (getMemberToLoanEntries() != null) {
			for (int i = 0; i < getSafeNumberOfWeeks(); i++) {
				MemberToLoanEntry entry = getMemberToLoanEntries().get(i);
				if (entry.getLocked() != null && entry.getLocked().booleanValue()) {
					lastDate = entry.getEndDate();
				} else {
					break;
				}
			}
		}
		if (lastDate != null) {
			// increment by one
			Calendar c = Calendar.getInstance();
			c.setTimeInMillis(lastDate.getTime());
			c.add(Calendar.DAY_OF_MONTH, 1);
			return c.getTime();
		}
		
		return lastDate;
	}
	

	private void validateLoanEntryAmounts() throws ValidationException {

	}
	
	public void setRepaymentRate(BigDecimal repaymentRate) {
		this.repaymentRate = repaymentRate;
	}
	
	public BigDecimal getRepaymentRate() {
		return repaymentRate;
	}
	
	public BigDecimal getOutstandingAmount() {
		return ModelUtils.subtract(getReturnAmount(), getAmountPaid());
	}
	
	public Date getRepaymentStartDate() {
		return repaymentStartDate;
	}
	
	public void setRepaymentStartDate(Date repaymentStartDate) {
		this.repaymentStartDate = repaymentStartDate;
	}
	
	public BigDecimal getAmountPaid() {
		return amountPaid;
	}
	
	public void setAmountPaid(BigDecimal amountPaid) {
		this.amountPaid = amountPaid;
	}
	
	public BigDecimal getInterestPercentage() {
		return interestPercentage;
	}

	public void setInterestPercentage(BigDecimal interestPercentage) {
		Object oldinterestPercentage = this.interestPercentage;
		this.interestPercentage = interestPercentage;
		firePropertyChange("interestPercentage", oldinterestPercentage,
				this.interestPercentage);
	}

	public void setMemberToLoanEntries(List<MemberToLoanEntry> memberToLoanEntries) {
		Object oldmemberToLoanEntries = this.memberToLoanEntries;
		this.memberToLoanEntries = memberToLoanEntries;
		firePropertyChange("memberToLoanEntries", oldmemberToLoanEntries,
				this.memberToLoanEntries);
	}

	@Transient
	public List<MemberToLoanHistory> getValidMemberToLoanHistories() {
		if (getHistoryIndex() == null) {
			return getMemberToLoanHistories();
		}
		return getMemberToLoanHistories().subList(0, getHistoryIndex());
	}
	
	public List<MemberToLoanEntry> getMemberToLoanEntries() {
		return memberToLoanEntries;
	}
	
	@Override
	public String toString() {
		if (sequenceID == null) {
			return "Loan ["+createStatusString()+"]";
		} else {
			return "Loan ["+formatID(this)+"] ["+createStatusString()+"]";
		}
	}

	public List<MemberToLoanHistory> getMemberToLoanHistories() {
		return memberToLoanHistories;
	}

	public void setMemberToLoanHistories(
			List<MemberToLoanHistory> memberToLoanHistories) {
		this.memberToLoanHistories = memberToLoanHistories;
	}
	
	public Long getSequenceID() {
		return sequenceID;
	}

	public void setSequenceID(Long sequenceID) {
		this.sequenceID = sequenceID;
	}

	public void setMember(Member member) {
		this.member = member;
	}

	public Member getMember() {
		return member;
	}

	public BigDecimal getLoanAmount() {
		return loanAmount;
	}

	public void setLoanAmount(BigDecimal loanAmount) {
		this.loanAmount = loanAmount;
	}
	
	public Date getStartDate() {
		return startDate;
	}

	public void setStartDate(Date startDate) {
		Object oldstartDate = this.startDate;
		this.startDate = startDate;
		firePropertyChange("startDate", oldstartDate, this.startDate);
	}

	public Date getFinalizedDate() {
		return finalizedDate;
	}
	
	public BigDecimal getReturnAmount() {
		if (getInterestPercentage() == null || getInterestPercentage().equals(0)) {
			return getLoanAmount();
		}
		if (getLoanAmount() != null) {
			return getLoanAmount().add(
					getLoanAmount().multiply(
							getInterestPercentage().divide(new BigDecimal(100),
									2, BigDecimal.ROUND_HALF_UP)));
		} else {
			return null;
		}
	}
	

	public void setFinalizedDate(Date firstEstimatedEndDate) {
		Object oldfirstEstimatedEndDate = this.finalizedDate;
		this.finalizedDate = firstEstimatedEndDate;
		firePropertyChange("firstEstimatedEndDate", oldfirstEstimatedEndDate,
				this.finalizedDate);
	}

	public int getNumberOfWeeks() {
		return numberOfWeeks;
	}

	public int getSafeNumberOfWeeks() {
		if (numberOfWeeks > MAXIMUM_NUMBER_OF_WEEKS) {
			return MAXIMUM_NUMBER_OF_WEEKS;
		}
		return numberOfWeeks;
	}


	public void setNumberOfWeeks(int numberOfWeeks) {
		Object oldnumberOfWeeks = this.numberOfWeeks;
		this.numberOfWeeks = numberOfWeeks;
		firePropertyChange("numberOfWeeks", oldnumberOfWeeks, this.numberOfWeeks);
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		Object olddescription = this.description;
		this.description = description;
		firePropertyChange("description", olddescription, this.description);
	}

	public LoanStatus getLoanStatus() {
		return loanStatus;
	}

	public void setLoanStatus(LoanStatus loanStatus) {
		this.loanStatus = loanStatus;
	}

	public Date getInsertStamp() {
		return insertStamp;
	}

	public void setInsertStamp(Date insertStamp) {
		this.insertStamp = insertStamp;
	}

	public Date getUpdateStamp() {
		return updateStamp;
	}

	public void setUpdateStamp(Date updateStamp) {
		this.updateStamp = updateStamp;
	}

	public Long getMemberID() {
		return memberID;
	}
	
	public Member getSecurity1() {
		return security1;
	}

	public void setSecurity1(Member security1) {
		this.security1 = security1;
	}

	public Member getSecurity2() {
		return security2;
	}

	public void setSecurity2(Member security2) {
		this.security2 = security2;
	}

	public Member getSecurity3() {
		return security3;
	}

	public void setSecurity3(Member security3) {
		this.security3 = security3;
	}

	public Long getSecurity1ID() {
		return security1ID;
	}

	public Long getSecurity2ID() {
		return security2ID;
	}

	public Long getSecurity3ID() {
		return security3ID;
	}

	public static String formatID(MemberToLoan loan) {
		return Member.formatID(loan.getMember().getId())+"-"+ModelUtils.formatID(loan.getSequenceID()+1, 2);
	}
	
}
